

class CssFile < Thing

  attribute :css_hash

  def initialize
    super
  end

  def default_template
    #add TitleThing.new
    #add TextThing.new
    self
  end

  def file_name
    'siteCss' #children[0].content
  end

  def render e
    Rack::Response.new(
      CssManager.new(css_hash || {}).to_s,
        200, {
          'Content-Type' => 'text/css',
          'Expires:' => Time.gm(Time.new.year() + 2).httpdate()
          }
    )
  end
end


class CssManager < MySingleton

  Event.on('app:init') {
    Event.on('client:css_change') { |e|
      instance.on_css_change e
    }
  }

  def self.initialize_singleton
    instance = wakeup
    instance
  end

  def self.wakeup
    begin
      return new CssFile.first().css_hash
    rescue
      return new {}
    end
  end

  def initialize rules = nil
    @css = rules || {}
  end

  def load_hash hash
    @css = hash
  end

  def on_css_change e
    if !e.data['value']
      remove_rule e.selector, e.attribute
    else
      set_rule e.selector, e.attribute, e.value
    end
    save
  end

  def save
    file = CssFile.first() || CssFile.new
    file.css_hash = to_hash.dup
    #p to_hash.dup
    file.save
  end

  def set_rule selector, attr, value
    attr = attr.strip
    if !value || value.strip.empty?
      remove_rule selector, attr
    else
      @css[selector] ||= {}
      @css[selector][attr] ||= {}
      @css[selector][attr] = value
    end
  end

  def remove_rule selector, attr
    return unless @css[selector]
    @css[selector].delete attr
    if @css[selector].keys.length == 0
      @css.delete selector
    end
  end

  def get_rule selector
    @css[selector] ||= {}
  end

  def get_rules
    @css.keys
  end

  def to_hash
    @css
  end

  def to_json
    to_hash.to_json
  end

  def export_css
    css = []
    @css.each_pair { |selector, rules|
      css << "#{selector}{#{export_rules rules}}"
    }
    css.join("\n")
  end

  def to_s
    export_css
  end

  def export_rules rules
    r = []
    rules.each_pair { |attr, value|
      r << (export_pair attr, value)
    }
    r.join(";")
  end

  def export_pair a, v
    a+':'+v
  end

  #reset
  instance


  class Test < AppTest
    def test_export_single_rule
      man = CssManager.new
      man.set_rule "a", "color", "blue"
      eq man.export_css, "a{color:blue}"
    end

    def test_export_multiple_rules
      man = CssManager.new
      man.set_rule "a", "color", "blue"
      man.set_rule "a", "text-decoration", "none"
      eq man.export_css, "a{color:blue;text-decoration:none}"
    end

    def test_export_multiple_groups
      man = CssManager.new
      man.set_rule "a", "color", "blue"
      man.set_rule "a:hover", "text-decoration", "none"
      eq man.export_css, "a{color:blue}\na:hover{text-decoration:none}"
    end

    def test_setting_empty_value_removes_rule
      man = CssManager.new
      man.set_rule "a", "color", "blue"
      man.set_rule "a", "color", ""
      eq (man.get_rule "a"), {}
    end

    def test_setting_whitespace_only_value_removes_rule
      man = CssManager.new
      man.set_rule "a", "color", "blue"
      man.set_rule "a", "color", "  "
      eq (man.get_rule "a"), {}
    end

    def test_removing_last_rule_removes_the_group
      man = CssManager.new
      man.set_rule "a", "color", "blue"
      man.set_rule "a", "color", ""
      eq man.get_rules, []
    end

    def test_removing_whitespace_in_attr_names
      man = CssManager.new
      man.set_rule "a", "color", "blue"
      man.set_rule "a", "color       ", "red"
      eq (man.get_rule "a"), 'color' => 'red'
    end

    def test_create_manager_from_css_hash
      h = { 'a' =>  {'color' => 'blue'}}
      eq CssManager.new(h).to_hash, h
    end

    def test_css_persistance
      DataMapper.setup(:test, :adapter => 'in_memory')
      repository(:test) {
        man = CssManager.new
        man.on_css_change Event.new('',
          :selector => "a",
          :attribute => "color",
          :value => "blue"
        )
        eq CssFile.first().css_hash,
          { 'a' =>  {'color' => 'blue'}}
      }
    end

    def test_wakeup_sets_persisted_css
      DataMapper.setup(:test, :adapter => 'in_memory')
      repository(:test) {
        file = CssFile.new()
        file.css_hash = { 'a' =>  {'color' => 'blue'}}
        file.save
        eq CssManager.wakeup.to_hash,
        { 'a' =>  {'color' => 'blue'}}          
      }
    end
  end
end



class CssEditorJs
  class Test < AppTest

    def setup
      @talk = false
      require 'snarl'
      @context = Snarl::JavascriptContext.new
      js IO.readlines('/public/js/css_editor.js').join
      js "var log = [];"
      (js "var console = {
          log:function() {            
            var ar = [];
            for (var a = arguments.length; a > 0; a--) {
             ar.push(arguments[a-1])
            }
            log.push.apply(log, [ar.reverse()]);
          }
        }
        var m = new modules[0]({Lang : {
            isObject : function() { return true; }
          }});
        var rules = new m.CssRules();
        ")
    end

    def talk
      @talk = true
    end

    def teardown
      return unless @talk
      puts "\n\n"
      log = js "log"
      unless log.empty?
        Hash[*log.each { |l|
          puts l.map { |el| (js_inspect el).inspect }.join(', ') #.map {|el| el.to_s }.inspect
        }]
      end
    end

    def js_inspect v
      if v.class.ancestors.include? Array
        v.map { |l|
          js_inspect l
        }
      elsif v.class.ancestors.include? Hash
        Hash[ *v.entries.flatten.map { |l| js_inspect l} ]
      else
        v
      end
    end

    def js js
      @context.eval js
    end

    def test_get_rule_by_one_tag_name
      eq js("
          rules.setRule('h1', {color : 'blue'});
          rules.getRule('h1');
        "),
        'color' => 'blue'
    end

    def test_get_rule_does_not_return_different_rules
      eq js("
          rules.setRule('h1', {color : 'red'});
          rules.setRule('h2', {color : 'green'});
          rules.setRule('h3', {color : 'blue'});
          rules.getRule('h2');
        "),
        'color' => 'green'
    end


    def test_get_rule_find_path_with_matching_tail
      eq js("
          rules.setRule('h1',   {color : 'red'});
          rules.setRule('h2 a', {color : 'green'});
          rules.setRule('h3',   {color : 'blue'});
          rules.getRule('body div h2 span a');
        "),
        'color' => 'green'
    end

    def test_path_with_unsatisfying_elements
      eq js("
          rules.setRule('h1 a',   {color : 'red'});
          rules.setRule('h2 a', {color : 'green'});
          rules.setRule('h3 a',   {color : 'blue'});
          rules.getRule('body div h2');
        "),
        {}
      eq js("
          rules.getRule('h2');
        "),
        {}
    end

    def test_get_rule_from_group_of_tag_names
      eq js("
          rules.setRule('h1',   {color : 'red'});
          rules.setRule('h1,h2,h3', {color : 'green'});
          rules.setRule('h2',   {color : 'blue'});
          rules.getRule('h3');
        "),
        'color' => 'green'
    end

    def test_get_groups_returns_matching_groups_by_priority_ascending
      js("
          rules.setRule('h1 a',   {color : 'black'});
          rules.setRule('a', {color : 'blue'});
          rules.setRule('body h1 a',   {color : 'red'});
          var groups = rules.getGroups('body h1 a');
        ")
      eq (js "groups.shift().pop()"), ['a', {'color' => 'blue'}]
      eq (js "groups.shift().pop()"), ['h1 a', {'color' => 'black'}]
      eq (js "groups.shift().pop()"), ['body h1 a', {'color' => 'red'}]
    end

    def test_sort_multiple_selector_rules_by_priority_asc
      js("
          rules.setRule('body a, h1 a', {color : 'black'});
          rules.setRule('a, body h1 a', {color : 'black'});
          var groups = rules.getGroups('body h1 a');
        ")
      eq (js "groups.shift().pop()"), ['body a, h1 a', {'color' => 'black'}]
      eq (js "groups.shift().pop()"), ['a, body h1 a', {'color' => 'black'}]
    end

    def test_load_css_hash
      js("
          rules.loadHash({'a': {color:'black'}});
          var groups = rules.getGroups('a');
        ")
      eq (js "groups.shift().pop()"), ['a', {'color' => 'black'}]
    end

    def test_override_rule
      js("
          rules.setRule('a', {color:'black'});
          rules.setRule('a', {color:'red'});
          var groups = rules.getGroups('a');
        ")
      eq (js "groups.length"), 1
      eq (js "groups[0][1]"), ['a', {'color' => 'red'}]
    end

    def test_override_with_empty_rule_removes_rule
      #talk
      js("
          rules.setRule('a', {color:'black', 'text-decoration':'none'});
          rules.setRule('a', {color:'', 'text-decoration':'none'});
          var groups = rules.getGroups('a');
        ")
      eq (js "groups[0][1][1]"), {'text-decoration' => 'none'}
    end

    def test_normalize_selector_trims_leading_and_trailing_whitspace
      eq (js "rules.normalizeSelector(' a ')"), "a"
    end

    def test_normalize_selector_removes_extra_whitspace
      eq (js "rules.normalizeSelector(' li\\n a ')"), "li a"
    end

    def test_css_rules_persistance_integration
      DataMapper.setup(:test, :adapter => 'in_memory')
      repository(:test) {
        css = CssManager.new()
        css.on_css_change Event.new(nil,
          :selector => 'a',
          :attribute => 'color',
          :value => 'blue'
        )
        CssFile.first
      }
    end
  end
end
